package com.ws.core.pagination;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import java.sql.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;

/**
 * mybatis分页拦截器的oracle实现
 * @author WS
 */
@Intercepts({
		@Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
				Object.class, RowBounds.class, ResultHandler.class}),
		@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}),
		@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PageInterceptorOracle implements Interceptor {
	protected static final Logger logger = LoggerFactory.getLogger(PageInterceptorOracle.class);
	/**
	 * 拦截器
	 *
	 * 拦截顺序 Executor -> StatementHandler -> ResultSetHandler
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {

		final Object target = invocation.getTarget();

		if(target instanceof Executor) {
			Optional<Pageable> pageableOptional = tryGetPageable(invocation.getArgs()[1]);
			pageableOptional.ifPresent(pageable -> {
				setExecutorPair((MappedStatement) invocation.getArgs()[0], pageable);
			});
		}
		else if(target instanceof StatementHandler){
			Connection connection = (Connection) invocation.getArgs()[0];
			rebuildMappedStatement((StatementHandler) target, connection);
		}
		else {
			ExecutorPair executorPair = executorPairThreadLocal.get();
			PageImpl page = new PageImpl(((List) tryProceed(invocation)), executorPair.pageable, executorPair.totalCount);
			executorPairThreadLocal.remove();
			return Stream.of(page).collect(toList());
		}

		return tryProceed(invocation);
	}

	private Object tryProceed(Invocation invocation) throws Throwable{
		try {
			return invocation.proceed();
		} catch (Throwable t) {
			executorPairThreadLocal.remove();
			throw t;
		}
	}

	private void setExecutorPair(MappedStatement mappedStatement, Pageable pageable) {
		initExecutorPair()
				.setPageable(pageable)
				.setMappedStatement(mappedStatement);
	}

	private void rebuildMappedStatement(StatementHandler statementHandler, Connection connection) {
		ExecutorPair executorPair = executorPairThreadLocal.get();

		MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
		String originalSql = (String)metaObject.getValue("delegate.boundSql.sql");
		executorPair.setOriginalSql(originalSql);

		executorPair.setTotalCount(getTotalCount(statementHandler, connection, executorPair.mappedStatement));

		if(executorPair.totalCount == 0) {
			metaObject.setValue("delegate.boundSql.parameterMappings", null);
			metaObject.setValue("delegate.boundSql.parameterObject", null);
		}
		metaObject.setValue("delegate.boundSql.sql", executorPair.getPageSql());
		if(logger.isDebugEnabled()) {
			logger.debug("生成的sql语句:" + metaObject.getValue("delegate.boundSql.sql"));
		}
	}

	private long getTotalCount(StatementHandler statementHandler, Connection connection, MappedStatement mappedStatement) {
		BoundSql boundSql = (BoundSql) SystemMetaObject.forObject(statementHandler).getValue("delegate.boundSql");
		String countSql = executorPairThreadLocal.get().getCountSql();
		BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
				boundSql.getParameterMappings(), boundSql.getParameterObject());

		Object additionalParameters = SystemMetaObject.forObject(boundSql)
				.getValue("additionalParameters");
		SystemMetaObject.forObject(countBS).setValue("additionalParameters",
														additionalParameters);
		SystemMetaObject.forObject(countBS).setValue("metaParameters",
													mappedStatement.getConfiguration()
															.newMetaObject(additionalParameters));
		try {
			PreparedStatement ps = connection.prepareStatement(countSql);
			ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBS);
			parameterHandler.setParameters(ps);
			ResultSet resultSet = ps.executeQuery();
			resultSet.next();
			return resultSet.getLong(1);
		} catch (SQLException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}


	/**
	 * 试图从参数中获得Pageable对象
	 * @param o Executor中query方法中的第二个参数,如果是单个参数就是参数本身,否则是一个map
	 */
	private Optional<Pageable> tryGetPageable(Object o) {
		Optional<Pageable> result;
		if(o instanceof Pageable)
			result = Optional.of((Pageable)o);
		else if(!(o instanceof Map))
			result = Optional.empty();
		else
			result = ((Map) o).values().stream().filter(v-> v instanceof Pageable).findFirst();

		if(!result.isPresent())
			executorPairThreadLocal.remove();

		return result;
	}



	@Override
	public Object plugin(Object target) {
		if (isNeedWarp(target)) {
			return Plugin.wrap(target, this);
		}
		return target;
	}

	@Override
	public void setProperties(Properties properties) {}

	/**
	 * 缓存在线程变量里的对象
	 *
	 * 在查询开始时创建，每个查询结束后清理
	 */
	private static class ExecutorPair {

		private String originalSql;
		private Pageable pageable;
		private MappedStatement mappedStatement;
		private long totalCount;

		private ExecutorPair setOriginalSql(String originalSql) {
			this.originalSql = originalSql.toLowerCase();
			return this;
		}

		private String getCountSql() {
			StringBuilder sql = new StringBuilder();
			sql.append("select count(1) from (").append(originalSql).append(" )");
			return sql.toString();
		}

		public ExecutorPair setPageable(Pageable pageable) {
			this.pageable = pageable;
			return this;
		}

		public ExecutorPair setMappedStatement(MappedStatement mappedStatement) {
			this.mappedStatement = mappedStatement;
			return this;
		}

		public ExecutorPair setTotalCount(long totalCount) {
			this.totalCount = totalCount;
			return this;
		}

		public String getPageSql() {
			if(totalCount == 0)
				return "select * from dual where DUMMY = '1'";

			String pageSql = originalSql;

			if(pageable.getSort() != null) {
				pageSql = buildOrder(originalSql);
			}
			if(pageable.getPageNumber() == 0)
				return String.format("select * from (%s) where rownum <= %d", pageSql, pageable.getPageSize());
			else
				return buildPageSql(pageSql);
		}

		private String buildPageSql(String pageSql) {
			String temp;
			if(isOrderSql(pageSql))
				temp = "select * from (select row_.*, rownum rownum_ from (select od.*,rownum from (%s) od) row_ where rownum <= %d) where rownum_ > %d";
			else
				temp = "select * from (select row_.*, rownum rownum_ from (%s) row_ where rownum <= %d) where rownum_ > %d";
			return String.format(temp, pageSql, pageable.getOffset()+pageable.getPageSize(), pageable.getOffset());
		}


		private boolean isOrderSql(String sql) {

			return sql.matches("[\\S\\s]*order\\s+by[^)]+");
		}

		private String buildOrder(String originalSql) {

			final StringBuilder orderSql = new StringBuilder(",");
			if(!isOrderSql(originalSql))
				orderSql.replace(0, 1, "order by ");

			if(pageable.getSort() != null) {
				pageable.getSort().forEach(order -> {
					orderSql.append(order.getProperty()).append(" ");
					orderSql.append(order.getDirection().toString()).append(",");
				});
			}
			orderSql.deleteCharAt(orderSql.length()-1);
			return originalSql + " " + orderSql.toString();
		}
	}

	private ExecutorPair initExecutorPair() {
		executorPairThreadLocal.set(new ExecutorPair());
		return executorPairThreadLocal.get();
	}

	/**
	 * 是否需要代理
	 *
	 * Executor会被代理，
	 * @param target 被代理的目标对象
	 */
	private boolean isNeedWarp(Object target) {
		if(target instanceof Executor)
			return true;

		if(executorPairThreadLocal.get() != null) {
			if(target instanceof ResultSetHandler || target instanceof StatementHandler)
				return true;
		}

		return false;
	}

	private ThreadLocal<ExecutorPair> executorPairThreadLocal = new ThreadLocal<>();
}
